using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.ObjectModel;
using System.Reflection;

namespace Oops.Xna.Framework.Graphics
{
	/// <summary>
	/// Represents a model that will be used for instancing.
	/// </summary>
	public class InstancedModel<T> : IDisposable where T : InstancedData 
	{
		#region InstancedModelMeshCollection Declaration

		/// <summary>
		/// Represents a read-only collection of <see cref="InstancedModelMesh<T>"/> instances.
		/// </summary>
		public class InstancedModelMeshCollection : ReadOnlyCollection<InstancedModelMesh<T>>
		{
			private InstancedModel<T> model;

			/// <summary>
			/// Initializes a new instance of the <see cref="InstanceModelMeshCollection"/> class.
			/// </summary>
			/// <param name="model">The parent <see cref="InstanceModel<T>"/>.</param>
			/// <param name="list">The collection of <see cref="InstanceModelMesh<T>"/> instances.</param>
			internal InstancedModelMeshCollection(InstancedModel<T> model, IList<InstancedModelMesh<T>> list) : base(list)
			{
				// Hold onto parameters.
				this.model = model;
			}
		}

		#endregion

		private GraphicsDevice graphicsDevice;

		private Model model;
		private InstancedModelMeshCollection meshes;
#if !XBOX
		private DynamicVertexBuffer[] instanceStreams;
#endif
		/// <summary>
		/// Initializes a new instance of the <see cref="InstancedModel"/> class.
		/// </summary>
		/// <param name="graphicsDevice">The associated <see cref="Microsoft.Xna.Framework.Graphics.GraphicsDevice"/>.</param>
		/// <param name="model">The source <see cref="Microsoft.Xna.Framework.Graphics.Model"/></param>
		public InstancedModel(GraphicsDevice graphicsDevice, Model model)
		{
			// The graphics device parameter must be specified.
			if (graphicsDevice == null)
			{
				throw new ArgumentNullException("graphicsDevice");
			}

			// The model parameter must be specified.
			if (model == null)
			{
				throw new ArgumentNullException("model");
			}

			// Hold onto parameters.
			this.graphicsDevice = graphicsDevice;
			this.model = model;

			// Create an instanced mesh for each model mesh.
			List<InstancedModelMesh<T>> list = new List<InstancedModelMesh<T>>(this.model.Meshes.Count);
			for (int i = 0; i < this.model.Meshes.Count; i++)
			{
				InstancedModelMesh<T> mesh = new InstancedModelMesh<T>(this.graphicsDevice, this.model.Meshes[i], this.model);
				list.Add(mesh);
			}
			this.meshes = new InstancedModelMeshCollection(this, list);
			for (int i = 0; i < this.meshes.Count; i++)
			{
				this.meshes[i].Model = this;
			}
		}

		~InstancedModel()
		{
			this.Dispose(false);
		}

#region IDisposable Members

		void IDisposable.Dispose()
		{
			this.Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion

		private void Dispose(bool disposing)
		{
#if !XBOX
			if (disposing)
			{
				// Dispose of the instancing streams.
				if (this.instanceStreams != null)
				{
					for (int i = 0; i < this.instanceStreams.Length; i++)
					{
						this.instanceStreams[i].Dispose();
					}
				}
			}
#endif
		}

		/// <summary>
		/// Gets the associated collection of <see cref="InstancedModelMesh<T>" /> instances.
		/// </summary>
		public InstancedModelMeshCollection Meshes
		{
			get { return this.meshes; }
		}

		/// <summary>
		/// Gets the skinning data from the associated <see cref="Model"/>.
		/// </summary>
		public SkinningData SkinningData
		{
			get { return this.model.Tag as SkinningData; }
		}

		public T CreateInstancedData(int size)
		{
			// Attempt to find a constructor on the type that has a Int32 argument.
			ConstructorInfo constructorInfo = typeof(T).GetConstructor(new Type[] { typeof(int) });
			if (constructorInfo == null)
			{
				throw new InvalidOperationException();
			}

			// Create the type with the specified size.
			T data = constructorInfo.Invoke(new object[] { size }) as T;
#if !XBOX
			// Create the necessary instance streams for the specified type.
			if (this.instanceStreams == null)
			{
				this.instanceStreams = new DynamicVertexBuffer[data.Properties.Count];				
			}

			// Iterate on the instancing data properties.
			for (int i = 0; i < data.Properties.Count; i++)
			{
				if (this.instanceStreams[i] == null || 
					this.instanceStreams[i].SizeInBytes < data.Properties[i].SizeOf * size)
				{
					// Dispose of the current instance stream.
					if (this.instanceStreams[i] != null)
					{
						this.instanceStreams[i].Dispose();
					}

					// Create the new instance stream of the necessary size.
					this.instanceStreams[i] = new DynamicVertexBuffer(
						this.graphicsDevice,
						data.Properties[i].SizeOf * size,
						BufferUsage.WriteOnly);
				}
			}

			// Update meshes' instance techniques.
			for (int i = 0; i < this.meshes.Count; i++)
			{
				this.meshes[i].SetInstancingTechnique(data);
			}
#endif
			// Return the instanced data...instance.
			return data;
		}

#if !XBOX
		/// <summary>
		/// Gets the instance streams for the <see cref="InstancedModel<T>" />.
		/// </summary>
		internal DynamicVertexBuffer[] InstanceStreams
		{
			get { return this.instanceStreams; }
		}
#endif
		public void Draw(InstancedData instancedData, ref Matrix view, ref Matrix projection)
		{
			for (int i = 0; i < this.meshes.Count; i++)
			{
				this.meshes[i].Draw(instancedData, ref view, ref projection);
			}
		}
	}
}
